Skip to content

Upgrade application to unblock infrastructure#68

Merged
JuanVqz merged 33 commits into
mainfrom
chore/upgrade-app
Jul 7, 2026
Merged

Upgrade application to unblock infrastructure#68
JuanVqz merged 33 commits into
mainfrom
chore/upgrade-app

Conversation

@JuanVqz

@JuanVqz JuanVqz commented Jul 6, 2026

Copy link
Copy Markdown
Member

What is this PR:

  • Bug fix
  • Feature
  • Chore

Description:

Upgrades the app's foundation and dev infrastructure so future upgrades are lower-risk, and fixes two production 500s uncovered while deploying this upgrade:

  • Ruby 2.7.2 → 3.2.11 and Rails 6.1 → 7.1 (via a dual-boot, hop-by-hop path: 6.1 → 7.0 → 7.1), landed together with the gem swaps each version needed (kt-paperclip, aws-sdk-s3, rubocop-rails-omakase), CI migrated from CircleCI to GitHub Actions, and config.load_defaults brought up to 7.1.
  • Docker + docker-compose added for local development, including a web_next service for exercising Gemfile.next side by side with web.
  • Test coverage: converted the suite from RSpec to Minitest, added coverage for the PDF export flow, and added a real system test (headless browser) covering the app's main path end to end — upload a Gemfile.lock, submit the form, and confirm both the results page and the PDF export render correctly.
  • Removed dead code and gems no longer needed (byebug, unused ActionCable scaffolding, stale helpers).
  • Fixes Kernel#open on remote Gemfile.lock downloads (app/models/gemfile.rb): Ruby 3.2's bundled open-uri (0.3.0) dropped its Kernel#open monkeypatch, so the bare open(uri) call fell through to a plain file open and raised an ArgumentError/Errno::ENOENT instead of fetching the remote file. Switched to URI.open(uri) explicitly, with a regression test.
  • Fixes the ruby-advisory-db clone failing silently on Heroku (heroku-26's runtime image ships no git binary at all, only the build image does): added an Aptfile + the heroku-community/apt buildpack so git is vendored into the runtime slug, and extracted the clone/update logic into BundlerAuditDatabasePreparer so failures are logged loudly (Rails.logger.error) instead of swallowed, with regression tests covering the success and failure paths.
  • README updated with the Docker workflow, the dual-boot pattern explanation, and why the apt buildpack is required.

Related links (if applicable):

Closes #64
Closes #63
Closes #59
Closes #60

How has this been tested?

  • Automated tests
  • Manual tests

What manual tests have been run?

  • Full test suite, rubocop, and reek pass clean on both Gemfile (7.1.6) and Gemfile.next.
  • Verified via docker compose up --build: app boots and serves the upload → results → PDF flow end to end.
  • Verified rake assets:precompile succeeds in production mode.
  • Deployed to bundler-audit-production and verified live: uploaded a real Gemfile.lock via curl, confirmed the results page (200) and PDF export (200, valid PDF) both render correctly with no errors in heroku logs.

JuanVqz added 3 commits July 6, 2026 12:43
Ruby 2.7.2/Rails 6.1 on Debian buster needs its apt sources pointed at
archive.debian.org (buster is EOL) and Node 12 installed from the
official tarball since NodeSource dropped its setup script. Debian
buster's glibc is also too old for nokogiri's precompiled native gem,
so bundler is configured to build it from source instead.

database.yml now reads host/username/password from env vars (defaulting
to the existing local behavior) so the same file works inside and
outside Docker. Mounting a volume over /usr/local/bundle would shadow
the gems baked into the image at build time, so docker-compose relies
on the image's built-in gem set instead.
Bumping the patch surfaced two transitive incompatibilities to fix
first: concurrent-ruby >= 1.3.5 breaks Rails' logger require order on
Ruby 2.7, and the nokogiri ~> 1.8.2 pin is too old for the loofah/
rails-html-sanitizer versions that ship with the newer Rails patch.

Test suite, rubocop, and reek all pass against 6.1.7.10.
Gemfile.next existed as an ad-hoc symlink pointing at rails/rails main
(unstable). Points it at the 7.0.0 release series instead, adds the
next_rails gem for NextRails.next? in application code and deprecation
tracking, and bakes both dependency sets into the Docker image so
`BUNDLE_GEMFILE=Gemfile.next` works without a separate install step.

Getting both sides to boot and pass required unpinning a few
long-stale dev/asset gems that only worked by accident on old Rails:
- font-awesome-rails: >= 4.7.0.9 (old version capped railties < 7)
- listen: >= 3.5 (Rails 7's evented file watcher needs ~> 3.5)
- spring / spring-watcher-listen: dropped from the `next?` side --
  the Rails-7-compatible spring 4.x requires Ruby >= 3.1, which is a
  separate hop; dev-only, not needed for CI or tests.

Both dependency sets boot, migrate, serve requests, and pass the full
test suite plus rubocop.
@JuanVqz JuanVqz self-assigned this Jul 6, 2026
@JuanVqz JuanVqz marked this pull request as ready for review July 6, 2026 19:32
JuanVqz added 11 commits July 6, 2026 13:50
Gemfile/Gemfile.lock now target 7.0.10 (was 6.1.7.10), matching
Gemfile.next/Gemfile.next.lock. The if next?/else conditional, the
next_rails gem, and the CircleCI build-current/build-next jobs stay in
place since the next planned hop is 7.0 -> 7.1 and this scaffolding
gets reused rather than torn down and rebuilt. Both branches
temporarily resolve to the same "~> 7.0.0" constraint; bump the
`if next?` branch to 7.1.0 to start that hop
(rubocop's Style/IdenticalConditionalBranches is disabled inline for
that block since the duplication is intentional here).

db/schema.rb picks up Rails 7's schema format (versioned
ActiveRecord::Schema[7.0], explicit precision: nil on datetime
columns) now that 7.0 is genuinely the default and not just the
dual-boot side.

Full test suite, rubocop, and reek pass clean.
GemfilesController#show passed template: "gemfiles/show.html.erb"
(literal extension included) and layout: "pdf.html" to wicked_pdf's
render call. Rails 6.1's template resolver tolerated the redundant
format/handler suffix; Rails 7.0's stricter resolver does not, so both
raised ActionView::MissingTemplate once 7.0 became the default.

Also, wicked_pdf forwards render's :formats straight through to
render_to_string. Without an explicit :formats, Rails used the current
request format (:pdf, since this runs inside format.pdf do...end) to
resolve the template, but the view is only authored as .html.erb.
6.1's format negotiation fell back to :html; 7.0's does not. Passing
formats: [:html] explicitly restores the original behavior.
Nothing exercised the format.pdf branch before, which is exactly the
code path the Rails 6.1 -> 7.0 upgrade silently broke. This would have
caught the regression fixed in the previous commit.
wkhtmltopdf-binary (0.12.3.1) ships a 32-bit x86 binary. On Apple
Silicon, running it under full arm64 emulation failed with "qemu-i386:
Could not open '/lib/ld-linux.so.2': No such file or directory" --
i386 is only a valid multiarch pairing alongside an amd64 base, not
arm64. Forcing the web service to build/run as linux/amd64 (Rosetta on
Apple Silicon) and installing libc6:i386/libstdc++6:i386 lets the
kernel run the 32-bit binary directly, no further emulation needed.

Verified: the PDF export request spec, which was silently failing
before with no signal locally, now runs the real wkhtmltopdf binary
end-to-end and passes.
Runs the Rails 7.1 dual-boot side (currently 7.0.10, same as the
default Gemfile) on port 3001, reusing the image already built for
`web` instead of a second build -- only BUNDLE_GEMFILE differs.

Not using `extends`: it merges array fields like `ports` instead of
overriding them, which leaked web's 3000:3000 mapping into web_next
too when tried. A plain service definition avoids that.

web and web_next share the same bind-mounted /app, so they'd otherwise
race on tmp/pids/server.pid and refuse to boot at the same time; gives
web_next its own --pid path and has the entrypoint clean up both.
web and web_next intentionally share one database (matches how dual-boot
is run in other upgrades here). Both containers run db:prepare on boot,
so starting or restarting them around the same time hits Rails'
migration advisory lock: the second one raised
ActiveRecord::ConcurrentMigrationError and the container exited.

The migration itself is a no-op once the first process finishes (both
sides are on the same schema during this hop), so retrying a few times
resolves it instead of failing the boot. Reproduced the race with a
simultaneous `docker restart` of both containers and confirmed the
retry fires and both come up healthy.
Resolves to 7.1.3.4, not the latest 7.1.6: activerecord 7.1.4+ uses
def method_missing(name, ...) (leading arg + ... forwarding), which is
Ruby 3.0+ only syntax and fails to even parse on our Ruby 2.7.2,
despite the rails gemspec still advertising ruby_version >= 2.7.0.
Confirmed by diffing activerecord/lib/active_record/attribute_methods.rb
across tags: absent through v7.1.3, present from v7.1.4 on. Cap stays
until Ruby is separately upgraded past 3.0.

bundle_report compatibility --rails-version=7.1.6 (and re-checked
against 7.1.3.4): 0 incompatible gems. Boot smoke test, full suite (11
examples), rubocop, and reek all pass clean on Gemfile.next.

Detection scan found one deprecation (config.cache_classes ->
config.enable_reloading) -- deferred to the commit that makes 7.1 the
default, since config/environments/*.rb are shared unconditionally
with the still-current Rails 7.0 side, which doesn't have
enable_reloading at all.
Gemfile/Gemfile.lock now target 7.1.3.4 (was 7.0.10), matching
Gemfile.next/Gemfile.next.lock. Both branches stay at the same
>= 7.1.0, < 7.1.4 range for now (capped below the Ruby-3.0-only
method_missing syntax in 7.1.4+); bump the if next? branch to start
the 7.1 -> 7.2 hop.

config.cache_classes -> config.enable_reloading (inverted boolean) in
all three environments, per Rails 7.1's deprecation. Deferred from the
dual-boot commit since config/environments/*.rb are shared
unconditionally and Rails 7.0 doesn't have enable_reloading at all.

db/schema.rb picks up Rails 7.1's schema version tag now that 7.1 is
genuinely the default.

Full test suite (11 examples), rubocop, and reek all pass clean.
paperclip (last real release 2018) calls URI.escape in
lib/paperclip/url_generator.rb on every asset URL it generates --
URI.escape was fully removed in Ruby 3.0. Confirmed even paperclip's
latest release (6.1.0, also 2018) never fixed this; the gem has had no
maintenance since Active Storage replaced it. This was a hard blocker
for any future Ruby 3.x upgrade.

kt-paperclip is an actively maintained fork (latest release 8.0.0)
that fixes this via URI::RFC2396_Parser instead, and stays drop-in
compatible -- same Paperclip:: module and require path, just a
different gem name (require: "paperclip" in the Gemfile line).

As a side effect, kt-paperclip 8.0.0 drops the mimemagic dependency
entirely, which also resolves an unrelated landmine: mimemagic 0.3.10
(our prior locked version) was yanked from RubyGems in 2020 over a
license violation, so any clean bundle install without a cache hit
would have failed regardless of Ruby version.

Verified end-to-end on the current stack (Ruby 2.7.2, Rails 7.1.3.4):
full test suite (11 examples), rubocop, and reek all pass. Manually
exercised the real upload -> HTML view -> PDF export flow via HTTP
(not rails runner) to exercise kt-paperclip's URL generation under
real request conditions -- no errors, and the "URI.escape is obsolete"
warnings that appeared in every previous test run are now gone.
Ports both spec files to Minitest, preserving every case including the
PDF-export coverage added earlier:
- spec/models/gemfile_spec.rb -> test/models/gemfile_test.rb (uses
  Bundler::Audit::Scanner.stub via minitest/mock instead of
  rspec-mocks; needed explicit `require "bundler/audit/scanner"` and
  `require "minitest/mock"` since nothing else forces those to load
  before the stub call, unlike RSpec where `RSpec.describe Gemfile`
  itself triggered the Gemfile model's autoload at file-parse time)
- spec/controllers/gemfiles_controller_spec.rb ->
  test/controllers/gemfiles_controller_test.rb, written as
  ActionDispatch::IntegrationTest (real routes, real view rendering,
  matching RSpec's `render_views` + `type: :controller` behavior)

Moved fixtures from spec/support/fixtures/ to the Rails-conventional
test/fixtures/files/. Deleted test/fixtures/gemfiles.yml -- it was a
stale default-scaffold fixture referencing a `file` column that has
never existed (Paperclip splits it into file_file_name etc.), dead
weight since the project used RSpec until now and never exercised it.

Removed rspec-rails and rubocop-rspec from the Gemfile (capybara /
selenium-webdriver stay -- Rails' own system tests need them
regardless of test framework). Stripped RSpec-only cops and stale
spec/ file references from .rubocop.yml / .rubocop_with_todo.yml /
.rubocop_todo.yml. Deleted config/spring.rb, dead config left over
from when the spring gem was dropped during the Rails 7.0 hop.

Updated CircleCI (both build-current and build-next) to run
`bin/rails test` instead of `bundle exec rspec`, README's documented
test command, and .gitignore's test-fixture storage path
(spec/test_files -> test/test_files, also updated in
config/environments/test.rb).

Also fixes a Rails 7.1 deprecation surfaced while writing the
integration test: config.action_dispatch.show_exceptions = false ->
:none.

Verified: full suite (11 runs, 35 assertions, 0 failures, 0 errors),
rubocop (49 files, no offenses), reek clean. Manually exercised the
real upload -> HTML view -> PDF export flow via HTTP end to end.
GemfileTest#stub_scanner and #valid_file don't touch instance state
by design (thin fixture/stub helpers), which reek flags as
UtilityFunction. Excluded them the same way the codebase already
excludes GemfilesController#vulnerabilities_count for the same cop.

This is what was actually failing CI (build-current and build-next),
not the test suite itself. Confirmed locally with an explicit path
(bundle exec reek .) -- bare `bundle exec reek` with no argument
reads from stdin when stdin isn't a TTY (reek's own
input_was_piped? check), which is exactly what happens under
non-interactive `docker compose run`, so every previous local reek
check in this session silently scanned nothing instead of the
codebase. CircleCI's invocation doesn't hit that path, which is why
CI caught what local testing didn't.
@JuanVqz JuanVqz changed the title Add Docker setup and prepare Rails 6.1 -> 7.0 dual-boot upgrade Rails 6.1 Upgrade Jul 6, 2026
JuanVqz added 2 commits July 6, 2026 18:11
Single matrixed job (gemfile: [Gemfile, Gemfile.next]) instead of two
duplicated jobs, replacing CircleCI's build-current/build-next split.
Each leg does rubocop, reek, db setup, asset precompile, and
bin/rails test.

Runs on GitHub-hosted ubuntu-latest (genuine x86_64, unlike our local
arm64 Docker setup), but still needs the i386 multiarch libs for
wkhtmltopdf-binary's bundled 32-bit binary -- ubuntu-latest doesn't
enable that by default even though it's already x86_64.

Both lint steps use explicit paths (`rubocop ... .`, `reek .`) rather
than bare invocations. reek specifically: bare `reek` with no path
falls back to reading stdin when stdin isn't a TTY, which silently
no-ops instead of scanning the codebase -- this bit local testing
earlier in this branch's history.

Verified locally with `act` (nektos/act) before pushing, which caught
two real bugs a plain YAML review wouldn't have:
- `libjpeg62-turbo` is a Debian package name (carried over from our
  Dockerfile, which targets Debian buster); ubuntu-latest is Ubuntu
  24.04 and doesn't have that package at all. Turned out to be
  unneeded anyway -- nothing in the app's asset pipeline or PDF
  templates actually processes JPEGs, and imagemagick pulls its own
  JPEG delegate transitively.
- Installing an old Node (to match .nvmrc, for ExecJS) via
  actions/setup-node breaks any later JS-based `uses:` step in the
  same job (ruby/setup-ruby included) -- GitHub Actions runs those on
  the runner's own Node, and swapping it out mid-job for an ancient
  version breaks them. ExecJS just needs *a* working JS runtime, not
  specifically Node 12, so installed it via apt instead of
  setup-node -- a plain binary on PATH, no runner-wide Node swap.

Not touched: whatever CircleCI project/webhook exists on
circleci.com's side for this repo. That's dashboard configuration,
not a repo file, and someone with access there should unlink it
separately.
Removes default Rails-scaffold comments that no longer add anything
(commented-out gem suggestions, restating what the line below already
says) and normalizes remaining string literals to double quotes.

The two comments explaining non-obvious constraints (concurrent-ruby
pinned below 1.3.5, Rails capped below 7.1.4) are dropped here too --
both are already documented in this branch's commit history
(25aea53, 613b896) if that context is needed again.
@JuanVqz JuanVqz changed the title Rails 6.1 Upgrade Upgrade application to unblock infrastructure Jul 7, 2026
JuanVqz added 3 commits July 6, 2026 19:09
app/helpers/home_helper.rb was a completely empty module
(`module HomeHelper; end`) -- never populated, never referenced from
any view. No mailer is ever actually sent either (ApplicationMailer is
just the default rails new scaffold, no subclass, no .deliver/mail()
call anywhere), but the mailer scaffold itself is left for now since
it wasn't explicitly flagged for removal.

test/integration/.keep and test/mailers/.keep were placeholders for
directories with no actual test files in them.
Replaces the standard/rubocop-rails combo with rubocop-rails-omakase,
the Rails-team-maintained lint config (bundled by default in fresh
Rails 7.1+ apps). Simpler than deciding how to keep standard's base
style and rubocop-rails' Rails cops in sync separately.

Getting there required a small dependency cascade:
- rubocop-rails-omakase pulls in a rubocop.yml that uses
  `EnforcedShorthandSyntax: either` / newer cop options not valid on
  our old locked rubocop (1.24.1) -- resolves on paper (gem version
  constraints are satisfied) but breaks at runtime (the config file
  itself isn't compatible with that cop implementation).
- rubocop was stuck at 1.24.1 because reek 6.0.4 pins
  `parser ~> 3.0.0`; newer rubocop needs a newer parser. reek's own
  latest release (6.5.0) fixes this (parser ~> 3.3.0) but itself
  requires Ruby >= 3.1.0 -- same "dev-only gem needs newer Ruby than
  this hop" situation as spring earlier in this branch. reek 6.1.4 is
  the newest release still on Ruby >= 2.6.0, and its parser ~> 3.2.0
  is enough to unblock rubocop up to 1.59.0, which the omakase config
  runs cleanly on.
- Fixed rubocop-rails-omakase's default EnforcedStyle: space for
  Layout/SpaceInside{Array,Hash}Literal{Brackets,Braces} back to
  no_space in .rubocop_todo.yml to match this codebase's existing
  convention, instead of reformatting every array/hash literal in the
  repo to add spaces. Also caught and reverted two pre-existing
  spaced literals (app/models/gemfile.rb,
  config/initializers/paperclip.rb, config/environments/production.rb)
  that were previously carved out via Exclude: entries for the
  opposite reason.
- A `bundle lock` run from the host (arm64 macOS) had polluted
  Gemfile.lock/Gemfile.next.lock's PLATFORMS with arm64-darwin,
  replacing the generic `ruby` platform entirely and pulling in a
  precompiled nokogiri-arm64-darwin gem the Linux container can't
  load. Fixed by forcing force_ruby_platform on the host too (matches
  the Docker image's own bundle config) and re-locking to `ruby` only.
- test/models/gemfile_test.rb needed an explicit `require "ostruct"`
  -- something in the relocked dependency chain no longer pulls it in
  transitively, and the file uses OpenStruct directly.

Also drops byebug (dev debugging gem): zero call sites anywhere in
app/ or lib/.

Verified: full test suite (11 runs, 35 assertions, 0 failures, 0
errors), rubocop (40 files, no offenses), reek (0 warnings) all pass
clean on both Gemfile and Gemfile.next.
Targets 3.2.x specifically (not just "3.x"): it satisfies both Rails
7.2's Ruby floor (>= 3.1.0) and Rails 8.0/8.1's (>= 3.2.0) in one hop,
so this doesn't need revisiting for the next two Rails hops.

One real hard blocker found and fixed: aws-sdk v2's aws-sdk-core
2.3.23 calls `Proc.new` relying on implicit block capture from the
caller, a Ruby feature fully removed in 3.0 -- raises
`ArgumentError: tried to create Proc object without a block` on any
boot. kt-paperclip's S3 storage adapter already targets aws-sdk-s3
(the modern per-service SDK, same Aws::S3:: namespace) directly, so
this was a straight gem swap with zero application code changes.

Everything else installed and booted cleanly on the first try --
confirmed by installing the full gem set locally under Ruby 3.2.11
before touching Docker/CI.

Re-added spring / spring-watcher-listen, dropped during the Rails 6.1
-> 7.0 hop specifically because spring 4.x needs Ruby >= 3.1 (now
satisfied). Needed config/spring.rb back too --
Spring.dangerously_allow_disabling_reloading = true, since this app's
test environment intentionally runs with config.enable_reloading =
false and Spring's reloader otherwise refuses to preload it. Also
bumped reek to its latest release (6.5.0, needs Ruby >= 3.1 -- was
capped at 6.1.4 during the rubocop-rails-omakase migration for the
same reason).

Dockerfile rebased from ruby:2.7.2 (Debian buster, EOL) to
ruby:3.2.11 (Debian 13 trixie, current). This removes three
buster-specific workarounds that no longer apply: the
archive.debian.org apt-source patch, the NodeSource-dropped-Node-12
tarball install (nodejs is a plain apt package on trixie), and
bundle's force_ruby_platform (buster's glibc was too old for
nokogiri's precompiled native gem; trixie's isn't). The wkhtmltopdf
32-bit-binary i386 multiarch libs stay -- that's an architecture
issue, not a Ruby or Debian-version one.

GitHub Actions' ruby-version bumped to match.

Verified: full gem install, full test suite (11 runs, 35 assertions,
0 failures, 0 errors), rubocop (41 files, no offenses), and reek (0
warnings) all pass on Ruby 3.2.11 across both Gemfile and
Gemfile.next. Manually exercised the real upload -> HTML view -> PDF
export flow via HTTP end to end -- confirms kt-paperclip, aws-sdk-s3,
and wkhtmltopdf all work together correctly under the new Ruby.
@JuanVqz JuanVqz had a problem deploying to bundler-audit-production July 7, 2026 05:41 Failure
@JuanVqz JuanVqz had a problem deploying to bundler-audit-production July 7, 2026 05:43 Failure
JuanVqz added 4 commits July 7, 2026 00:06
…Cable/ActionText JS

Rails unconditionally adds activestorage, actioncable, and actiontext JS to
config.assets.precompile even when those frameworks are unused (this app
uploads via Paperclip, has no rich text, no websockets). Their ES6 syntax
(const, class, spread) fails Uglifier's ES5-only parser, crashing
`rake assets:precompile` in production with Uglifier::Error.

Also drop app/assets/javascripts/cable.js: a dead default-generated file
that required action_cable directly, pulling actioncable.js into the
compile graph via application.js's require_tree regardless of the
precompile_assets flag.

ActiveStorage/ActionCable have official precompile_assets opt-out flags.
ActionText doesn't, so its entries are subtracted in config.after_initialize
(its own engine initializer runs after config/initializers and would
otherwise re-add them).

Also add a placeholder config/storage.yml: ActiveStorage's lazy
on_load(:active_storage_blob) hook raises if it's missing, even though this
app doesn't use ActiveStorage for uploads.
load_defaults 5.1 implies cache_format_version 6.1. Rails 7.1 deprecates
support for that format (removed in 7.2). Bump just this setting ahead of
the full load_defaults alignment to silence the warning now.
This app never used ActionCable (no channels beyond the default
generated scaffold, no mount ActionCable::Server, nothing subclassing
ApplicationCable::Channel). Delete the dead app/channels/application_cable
scaffold files.

Bump config.load_defaults from 5.1 to 7.1 to match the actual Rails gem
version now in use. This also subsumes the earlier explicit
cache_format_version = 7.0 override (7.1's load_defaults sets it directly),
so that line is removed as redundant.

Verified: full test suite green on both Gemfile and Gemfile.next, rubocop
and reek clean, and a real HTTP smoke test of the upload -> HTML -> PDF
golden path against a running server on the new defaults.
Follow-up to 5303c9f: this change was written and verified together with
the ActionCable scaffold removal but was dropped from that commit by a
staging mistake (git add on a path that no longer existed after git rm
short-circuited the rest of the add command).

Bump config.load_defaults from 5.1 to 7.1 to match the actual Rails gem
version in use. Subsumes the earlier explicit cache_format_version = 7.0
override (7.1's load_defaults sets it directly), so that line is removed
as redundant.
@JuanVqz JuanVqz temporarily deployed to bundler-audit-production July 7, 2026 13:57 Inactive
JuanVqz added 4 commits July 7, 2026 08:13
Drives an actual browser (headless Chrome/Chromium) through the app's real
entry point: visit the home page, attach a Gemfile.lock, click the upload
form's submit button (JS-gated -- it starts disabled and is enabled on file
change, which requires a JS-capable driver, not rack_test), assert the
redirect to the results page and that vulnerabilities actually rendered,
then verify the PDF export endpoint returns a real PDF.

Bumped capybara 2.18 -> 3.40 (the old pin predates Rails 7.1's system-test
integration and predates this app ever having a real system test). Added
chromium + chromium-driver to the Dockerfile for local/dev runs, and a
`bin/rails test:system` CI step (bin/rails test does not run test/system by
default). The driver registration in application_system_test_case.rb checks
several known binary paths (Debian's chromium in Docker, Google Chrome on
GitHub Actions' ubuntu-latest runners) and falls back to Selenium Manager's
own auto-resolution rather than hardcoding one environment's layout.

Verified locally: full suite green on both Gemfile and Gemfile.next,
rubocop/reek clean. The system test itself could not be run to completion
locally -- this machine forces the whole web container to linux/amd64 (for
wkhtmltopdf's 32-bit binary), and amd64 Chromium crashes under Rosetta 2
emulation on Apple Silicon (a Rosetta JIT/BRK-instruction translation bug,
confirmed independent of headless mode or JIT flags). GitHub Actions'
ubuntu-latest runners are genuine x86_64 hardware with no emulation
involved, so this is a local-machine-only limitation, not a defect in the
test or the CI wiring -- verifying via the real CI run next.
The custom upload widget visually hides the real <input type=file> behind
styled label/span elements. Capybara's attach_file needs make_visible: false
to temporarily reveal it for the attach (confirmed against real CI --
GitHub Actions' genuine x86_64 runners, unlike this local Apple Silicon
machine, can actually launch headless Chrome to run this test).
Step 0 of the next hop: get onto the latest patch of the current minor
before starting 7.1 -> 7.2. The prior "< 7.1.4" cap was there because 7.1.4+
uses def method_missing(name, ...) syntax that needs Ruby >= 3.0; now moot
since the app is on Ruby 3.2.11.

Two real findings from the bump:
- `bundle update rails` transitively pulled minitest to 6.0.6, which dropped
  minitest/mock.rb into a separate minitest-mock gem (LoadError on the
  existing gemfile_test.rb). Pinned minitest below 6 so a Rails patch bump
  doesn't silently drag the test framework across an unrelated major version.
- 7.1.6 surfaced a new deprecation: config/secrets.yml / Rails.application.secrets
  is removed in 7.2, our very next hop. Moved secret_key_base directly into
  each environment config (same values, no credentials-file migration) and
  deleted secrets.yml.

Verified: full suite green on both Gemfile and Gemfile.next, rubocop/reek
clean, and a real HTTP smoke test of the upload -> HTML -> PDF flow to
confirm cookies/CSRF still work under the new secret_key_base config.
make_visible: false disables Capybara's own visibility-forcing behavior --
the opposite of what's needed for a custom widget that CSS-hides the real
file input. Confirmed against real CI (genuine x86_64 GitHub Actions
runners): Chrome/Puma launched fine both times, the failure was purely
Capybara refusing to interact with the still-hidden element.
Ruby 3.2's bundled open-uri (0.3.0) dropped its Kernel#open monkeypatch,
so open(uri) fell through to a plain file open and raised Errno::ENOENT
on the S3 URL instead of fetching it.
@JuanVqz JuanVqz temporarily deployed to bundler-audit-production July 7, 2026 16:21 Inactive
Bundler::Audit::Database.update! rescues DownloadFailed/UpdateFailed and
returns false, so if the git clone fails at boot the app starts fine but
the advisory DB directory never gets created. Every subsequent audit then
500s with ArgumentError ("... is not a directory") from Scanner.new, with
no trace of why. Log the real underlying error instead.
@JuanVqz JuanVqz temporarily deployed to bundler-audit-production July 7, 2026 16:35 Inactive
heroku-26's runtime image dropped git entirely (only the build image has
it), so Bundler::Audit::Database's runtime `git clone` of ruby-advisory-db
always failed with DownloadFailed, silently, on every boot. Needs the apt
buildpack added ahead of heroku/ruby to actually take effect.
@JuanVqz JuanVqz temporarily deployed to bundler-audit-production July 7, 2026 16:42 Inactive
@JuanVqz JuanVqz force-pushed the chore/upgrade-app branch from 119f449 to a613cba Compare July 7, 2026 16:57
@JuanVqz JuanVqz temporarily deployed to bundler-audit-production July 7, 2026 16:58 Inactive
JuanVqz added 3 commits July 7, 2026 11:03
Moves the ruby-advisory-db clone/update logic out of the initializer and
into a plain class so it's actually testable. Covers the fresh-clone path,
the update-existing-checkout path, and the failure path (DownloadFailed /
UpdateFailed) to make sure failures keep logging loudly instead of
regressing back to a silent swallow.
…onale

Requirements/setup were stale (Ruby 2.7.2, no Docker, no mention of the
dual-boot Gemfile.next pattern). Adds a production-notes section
explaining why the heroku-community/apt buildpack + Aptfile are required
now that heroku-26 dropped git from the runtime image.
Matches the existing convention for other test classes (GemfileTest,
BundlerAuditScannerStub) in .reek.yml -- these are minitest fixtures, not
app modules that need doc comments.
@JuanVqz JuanVqz merged commit 859bb8f into main Jul 7, 2026
2 checks passed
@JuanVqz JuanVqz deleted the chore/upgrade-app branch July 7, 2026 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upgrade to Rails 7.1 Upgrade to Rails 7.0 Uploading a Gemfile.lock returns 505 code status Unhandled Insecure Source URI

1 participant